Golang1.x版本泛型编程

Go是一门天生为服务器程序设计的简洁的语言,因此Go的设计原则聚焦在可扩展性,可读性和并发性,而多态性并不是这门语言的设计初衷,因此就被放在了一边。虽然在2.0版本之前还没有泛型的支持,但是go自带的一些语言特性可以满足一些类似“泛型”的要求,比如内置类型:

  • array
  • slice
  • map
  • chan
    这四种类型可以用任意类型的元素初始化,例如map[yourtype]bool就可以用来实现任意元素的集合。go的一些内置函数也是通用的,比如len()既可以用于string, array, slice, 也可以用于求map的长度。

但是如果golang内置结构和函数不能满足需求,可以从以下几种方法来入手:

  1. 类型断言
    当你想实现一个通用函数的时候,会考虑传入的参数是不固定类型的,go正好提供了interface{}类型,它可以代表任意类型。当你不确定用什么类型合适的时候,用它就对了。
    来个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Container struct {
elem []interface{}
}

func (this *Container) Put(v interface{}) {
*this = append(*this, elem)
}
// 取出最后一个元素
func (this *Container) Get() interface{} {
ret := (*c)[len(*c)-1]
*c = (*c)[:len(*c)-1]
return ret
}

func assertExample() {
container := &Container{}
container.Put(1)
container.Put("Hello")
_, ok := container.Get().(int);!ok {
fmt.Println("Unable to read an int from container")
}
}

通过接口类型我们把细节完全隐藏了起来,但是我们也把运行时类型检查失败的风险留给了调用者,而且调用者每次都要写 _, ok := YourType.(type);!ok{} 这种类型断言,比较啰嗦。

  1. 反射机制
    反射机制就是在运行时动态的调用对象的方法和属性,Python和Java语言都有实现,golang是通过reflect包来实现的,反射机制允许程序在编译时处理未知类型的对象,这听上去可以解决我们上面的问题,现在修改一下代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    type Container struct {
    elem reflect.Value
    }
    func NewContainer(t reflect.Type) *Container {
    return &Container{
    elem: reflect.MakeSlice(reflect.SliceOf(t), 0, 10),
    }
    }
    func (this *Container) Put(v interface{}) {
    if reflect.ValueOf(val).Type() != c.elem.Type().Elem() {
    panic(fmt.Sprintf("Cannot set a %T into a slice of %s", val, c.elem.Type().Elem()))
    }
    c.elem = reflect.Append(c.elem, reflect.ValueOf(val))
    }
    func (this *Container) Get(regref interface{}) interface{} {
    retref = c.elem.Index(c.elem.Len()-1)
    c.elem = c.elem.Slice(0, c.elem.Len()-1)
    return retref
    }
    func assertExample() {
    a := 0.123456
    nc := NewContainer(reflect.TypeOf(a))
    nc.Put(a)
    nc.Put(1.11)
    nc.Put(2.22)
    b := 0.0
    c := nc.Get(&b)
    fmt.Println(c)
    d := nc.Get(&b)
    fmt.Println(d)
    }

可以看到使用反射的代码量要增加不少,而且要写各种reflect方法的前缀,对于有代码洁癖的人来说是个灾难,也会让程序员效率变慢,因为没有编译时的类型检查,会带来额外的运行时开销。

  1. 使用接口
    接口有个特点是只做定义不管细节实现,可以利用这一特性实现泛型,例如标准库中的sort包就是使用接口实现对任意容器元素排序的例子
    1
    2
    3
    4
    5
    type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
    }

只要实现接口定义的这三种方法即可对自定义的容器元素进行排序,具体例子可以参考sort包的文档。查看sort包的源码后可以发现代码非常简洁,但是缺点也很明显,使用者需要自己把接口方法重新实现一遍。

  1. 代码生成工具
    代码生成的原理是先写一个mock类型,这个mock只做占位符使用,然后通过转换工具把这个占位符替换成具体类型,已经有很多人写过了这里不再多说,缺点是生成的文件比较大,依赖第三方工具和模板语法。

总之,golang的泛型实现没有一个固定的方法,或者说一个放之四海而皆准的理想方法,程序设计达到一定规模后,总是需要在代码效率,编译器效率和运行效率之间做出一些取舍,因此我们需要知道实现泛型的不同方法,在适当的时候使用合适的那个。